/******************************************************************************* * Copyright (c) 2004, 2013 John-Mason P. Shackelford and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * John-Mason P. Shackelford - initial API and implementation * IBM Corporation - Bug 73411, 84342, on-going bug fixing *******************************************************************************/ package org.eclipse.ant.internal.ui.editor.formatter; import java.text.CharacterIterator; import java.text.StringCharacterIterator; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class XmlTagFormatter { public static class AttributePair { private String fAttribute; private String fValue; private char fQuote; public AttributePair(String attribute, String value, char attributeQuote) { fAttribute = attribute; fValue = value; fQuote = attributeQuote; } public String getAttribute() { return fAttribute; } public String getValue() { return fValue; } public char getQuote() { return fQuote; } } protected static class ParseException extends Exception { private static final long serialVersionUID = 1L; public ParseException(String message) { super(message); } } protected static class Tag { private List<AttributePair> fAttributes = new ArrayList<>(); private boolean fClosed; private String fElementName; public void addAttribute(String attribute, String value, char quote) { fAttributes.add(new AttributePair(attribute, value, quote)); } public int attributeCount() { return fAttributes.size(); } public AttributePair getAttributePair(int i) { return fAttributes.get(i); } public String getElementName() { return this.fElementName; } public boolean isClosed() { return fClosed; } public int minimumLength() { int length = 2; // for the < > if (this.isClosed()) length++; // if we need to add an /> length += this.getElementName().length(); if (this.attributeCount() > 0 || this.isClosed()) length++; for (int i = 0; i < this.attributeCount(); i++) { AttributePair attributePair = this.getAttributePair(i); length += attributePair.getAttribute().length(); length += attributePair.getValue().length(); length += 4; // equals sign, quote characters & trailing space } if (this.attributeCount() > 0 && !this.isClosed()) length--; return length; } public void setAttributes(List<AttributePair> attributePair) { fAttributes.clear(); fAttributes.addAll(attributePair); } public void setClosed(boolean closed) { fClosed = closed; } public void setElementName(String elementName) { fElementName = elementName; } @Override public String toString() { StringBuffer sb = new StringBuffer(500); sb.append('<'); sb.append(this.getElementName()); if (this.attributeCount() > 0 || this.isClosed()) sb.append(' '); for (int i = 0; i < this.attributeCount(); i++) { AttributePair attributePair = this.getAttributePair(i); sb.append(attributePair.getAttribute()); sb.append('='); sb.append(attributePair.getQuote()); sb.append(attributePair.getValue()); sb.append(attributePair.getQuote()); if (this.isClosed() || i != this.attributeCount() - 1) sb.append(' '); } if (this.isClosed()) sb.append('/'); sb.append('>'); return sb.toString(); } } protected static class TagFormatter { private int countChar(char searchChar, String inTargetString) { StringCharacterIterator iter = new StringCharacterIterator(inTargetString); int i = 0; if (iter.first() == searchChar) i++; while (iter.getIndex() < iter.getEndIndex()) { if (iter.next() == searchChar) { i++; } } return i; } public String format(Tag tag, FormattingPreferences prefs, String indent, String lineDelimiter) { if (prefs.wrapLongTags() && lineRequiresWrap(indent + tag.toString(), prefs.getMaximumLineWidth(), prefs.getTabWidth())) { return wrapTag(tag, prefs, indent, lineDelimiter); } return tag.toString(); } protected boolean lineRequiresWrap(String line, int lineWidth, int tabWidth) { return tabExpandedLineWidth(line, tabWidth) > lineWidth; } /** * @param line * the line in which spaces are to be expanded * @param tabWidth * number of spaces to substitute for a tab * @return length of the line with tabs expanded to spaces */ protected int tabExpandedLineWidth(String line, int tabWidth) { int tabCount = countChar('\t', line); return (line.length() - tabCount) + (tabCount * tabWidth); } protected String wrapTag(Tag tag, FormattingPreferences prefs, String indent, String lineDelimiter) { StringBuffer sb = new StringBuffer(1024); sb.append('<'); sb.append(tag.getElementName()); sb.append(' '); if (tag.attributeCount() > 0) { AttributePair pair = tag.getAttributePair(0); sb.append(pair.getAttribute()); sb.append('='); sb.append(pair.getQuote()); sb.append(tag.getAttributePair(0).getValue()); sb.append(pair.getQuote()); } if (tag.attributeCount() > 1) { char[] extraIndent = new char[tag.getElementName().length() + 2]; Arrays.fill(extraIndent, ' '); for (int i = 1; i < tag.attributeCount(); i++) { AttributePair pair = tag.getAttributePair(i); sb.append(lineDelimiter); sb.append(indent); sb.append(extraIndent); sb.append(pair.getAttribute()); sb.append('='); sb.append(pair.getQuote()); sb.append(pair.getValue()); sb.append(pair.getQuote()); } } if (prefs.alignElementCloseChar()) { sb.append(lineDelimiter); sb.append(indent); } else if (tag.isClosed()) { sb.append(' '); } if (tag.isClosed()) sb.append('/'); sb.append('>'); return sb.toString(); } } // if object creation is an issue, use static methods or a flyweight // pattern protected static class TagParser { private String fElementName; private String fParseText; protected List<AttributePair> getAttibutes(String elementText) throws ParseException { class Mode { private int mode; public void setAttributeNameSearching() { mode = 0; } public void setAttributeNameFound() { mode = 1; } public void setAttributeValueSearching() { mode = 2; } public void setAttributeValueFound() { mode = 3; } public void setFinished() { mode = 4; } public boolean isAttributeNameSearching() { return mode == 0; } public boolean isAttributeNameFound() { return mode == 1; } public boolean isAttributeValueSearching() { return mode == 2; } public boolean isAttributeValueFound() { return mode == 3; } public boolean isFinished() { return mode == 4; } } List<AttributePair> attributePairs = new ArrayList<>(); CharacterIterator iter = new StringCharacterIterator(elementText.substring(getElementName(elementText).length() + 2)); // state for finding attributes Mode mode = new Mode(); mode.setAttributeNameSearching(); char attributeQuote = '"'; StringBuffer currentAttributeName = null; StringBuffer currentAttributeValue = null; char c = iter.first(); while (iter.getIndex() < iter.getEndIndex()) { switch (c) { case '"': case '\'': if (mode.isAttributeValueSearching()) { // start of an attribute value attributeQuote = c; mode.setAttributeValueFound(); currentAttributeValue = new StringBuffer(1024); } else if (mode.isAttributeValueFound() && attributeQuote == c) { // we've completed a pair! AttributePair pair = new AttributePair(currentAttributeName.toString(), currentAttributeValue.toString(), attributeQuote); attributePairs.add(pair); // start looking for another attribute mode.setAttributeNameSearching(); } else if (mode.isAttributeValueFound() && attributeQuote != c) { // this quote character is part of the attribute value currentAttributeValue.append(c); } else { // this is no place for a quote! throw new ParseException("Unexpected '" + c //$NON-NLS-1$ + "' when parsing:\n\t" + elementText); //$NON-NLS-1$ } break; case '=': if (mode.isAttributeValueFound()) { // this character is part of the attribute value currentAttributeValue.append(c); } else if (mode.isAttributeNameFound()) { // end of the name, now start looking for the value mode.setAttributeValueSearching(); } else { // this is no place for an equals sign! throw new ParseException("Unexpected '" + c //$NON-NLS-1$ + "' when parsing:\n\t" + elementText); //$NON-NLS-1$ } break; case '/': case '>': if (mode.isAttributeValueFound()) { // attribute values are CDATA, add it all currentAttributeValue.append(c); } else if (mode.isAttributeNameSearching()) { mode.setFinished(); } else if (mode.isFinished()) { // consume the remaining characters } else { // we aren't ready to be done! throw new ParseException("Unexpected '" + c //$NON-NLS-1$ + "' when parsing:\n\t" + elementText); //$NON-NLS-1$ } break; default: if (mode.isAttributeValueFound()) { // attribute values are CDATA, add it all currentAttributeValue.append(c); } else if (mode.isFinished()) { if (!Character.isWhitespace(c)) { throw new ParseException("Unexpected '" + c //$NON-NLS-1$ + "' when parsing:\n\t" + elementText); //$NON-NLS-1$ } } else { if (!Character.isWhitespace(c)) { if (mode.isAttributeNameSearching()) { // we found the start of an attribute name mode.setAttributeNameFound(); currentAttributeName = new StringBuffer(255); currentAttributeName.append(c); } else if (mode.isAttributeNameFound()) { currentAttributeName.append(c); } } } break; } c = iter.next(); } if (!mode.isFinished()) { throw new ParseException("Element did not complete normally."); //$NON-NLS-1$ } return attributePairs; } /** * @param tagText * text of an XML tag * @return extracted XML element name */ protected String getElementName(String tagText) throws ParseException { if (!tagText.equals(this.fParseText) || this.fElementName == null) { int endOfTag = tagEnd(tagText); if ((tagText.length() > 2) && (endOfTag > 1)) { this.fParseText = tagText; this.fElementName = tagText.substring(1, endOfTag); } else { throw new ParseException("No element name for the tag:\n\t" //$NON-NLS-1$ + tagText); } } return fElementName; } protected boolean isClosed(String tagText) { return tagText.charAt(tagText.lastIndexOf('>') - 1) == '/'; } /** * @param tagText * @return a fully populated tag */ public Tag parse(String tagText) throws ParseException { Tag tag = new Tag(); tag.setElementName(getElementName(tagText)); tag.setAttributes(getAttibutes(tagText)); tag.setClosed(isClosed(tagText)); return tag; } private int tagEnd(String text) { // This is admittedly a little loose, but we don't want the // formatter to be too strict... // http://www.w3.org/TR/2000/REC-xml-20001006#NT-Name for (int i = 1; i < text.length(); i++) { char c = text.charAt(i); if (!Character.isLetterOrDigit(c) && c != ':' && c != '.' && c != '-' && c != '_') { return i; } } return -1; } } public static String format(String tagText, FormattingPreferences prefs, String indent, String lineDelimiter) { Tag tag; if (tagText.startsWith("</") || tagText.startsWith("<%") //$NON-NLS-1$ //$NON-NLS-2$ || tagText.startsWith("<?") || tagText.startsWith("<[")) { //$NON-NLS-1$ //$NON-NLS-2$ return tagText; } try { tag = new TagParser().parse(tagText); } catch (ParseException e) { // if we can't parse the tag, give up and leave the text as is. return tagText; } return new TagFormatter().format(tag, prefs, indent, lineDelimiter); } }